ZA | 25-SDC-July | Luke Manyamazi | Sprint 4 | Python Implement Shell Tools Exercises#148
Conversation
illicitonion
left a comment
There was a problem hiding this comment.
This is generally looking good, but I left a few things to think about :)
implement-shell-tools/cat/cat.py
Outdated
| import glob | ||
| import argparse | ||
|
|
||
| def cat(filepath, n=False, b=False, line_counter=None): |
There was a problem hiding this comment.
n and b are both:
- Not very expressive names
- Seem mutually exclusive - it doesn't make sense for someone to pass both.
Can you think of a way of passing this information into the function which is easier to read, and makes more clear that only one of these should be set?
implement-shell-tools/cat/cat.py
Outdated
| else: | ||
| print(line, end='') | ||
| elif n: | ||
| print(f"{line_counter[0]:6}\t{line}", end='') |
There was a problem hiding this comment.
There's some repetition here between the b case and n case - imagine we wanted to start padding the line number by 8 instead of 6 characters - we'd need to change that in two places. Can you think how to avoid that?
There was a problem hiding this comment.
We can create a single helper function for printing a numbered line. This avoids duplicating the f"{line_counter[0]:6}" formatting. Then in cat(), both the “all lines” and “non-empty lines” cases can just call this helper.
implement-shell-tools/cat/cat.py
Outdated
|
|
||
| files = [] | ||
| for pattern in args.files: | ||
| files.extend(glob.glob(pattern) or [pattern]) |
There was a problem hiding this comment.
It's interesting you're globbing here - I would expect a shell to handle this. What would break if you removed the glob expansion yourself?
implement-shell-tools/cat/cat.py
Outdated
| if b: | ||
| if line.strip(): | ||
| print(f"{line_counter[0]:6}\t{line}", end='') | ||
| line_counter[0] += 1 |
There was a problem hiding this comment.
Why is this value in an array? You always seem to look at exactly one value in the array?
implement-shell-tools/cat/cat.py
Outdated
| import glob | ||
| import argparse | ||
|
|
||
| def cat(filepath, n=False, b=False, line_counter=None): |
There was a problem hiding this comment.
It looks like you always pass values for these arguments - why are you supplying default values for them? Particularly for line_counter where if None is used your code will actually break.
| except FileNotFoundError: | ||
| print(f"cat: {filepath}: No such file or directory") | ||
|
|
||
| def main(): |
There was a problem hiding this comment.
How did you test this implementation?
I created two files and tried using cat -n /file/1 /file/2 and cat -b /file/1 /file/2 and compared the output with using your script, and didn't always get the same results
There was a problem hiding this comment.
This comment still stands - I get different results between your program and the builtin cat.
implement-shell-tools/ls/ls.py
Outdated
| import os | ||
| import argparse | ||
|
|
||
| def ls(path='.', one_column=False, show_hidden=False): |
There was a problem hiding this comment.
Same question as above about default args
implement-shell-tools/wc/wc.py
Outdated
| if count_words: parts.append(str(len(words))) | ||
| if count_bytes: parts.append(str(bytes_)) | ||
|
|
||
| if not parts: |
There was a problem hiding this comment.
This works, but it feels like it's testing for a side-effect ("We didn't add any paths") rather than the cause ("you didn't ask for any of lines, words, or bytes") - I'd maybe rewrite this if to be framed in what the user requested.
(But the code works fine as it is, too)
| parser.add_argument('paths', nargs='+', help='Files to count') | ||
| args = parser.parse_args() | ||
|
|
||
| for path in args.paths: |
There was a problem hiding this comment.
If I pass multiple files, the real wc outlines a line that you don't - can you add that too?
35a4349 to
ff86f10
Compare
…for all the files
|
cat.py Removed the mutable list [1] hack for line numbering; now cat() returns the updated line number. ls.py wc.py |
|
Hi @illicitonion, please kindly review and see if all the requirements were met. If so please mark the PR as complete. Thank you. |
illicitonion
left a comment
There was a problem hiding this comment.
This is generally looking good, but there are a bunch of small details and consistencies to look at :)
| else: | ||
| print(line, end='') | ||
| except FileNotFoundError: | ||
| print(f"cat: {filepath}: No such file or directory") |
There was a problem hiding this comment.
Does this print to stdout or stderr? Which should it print to? Why?
| except FileNotFoundError: | ||
| print(f"cat: {filepath}: No such file or directory") | ||
|
|
||
| def main(): |
There was a problem hiding this comment.
This comment still stands - I get different results between your program and the builtin cat.
| except NotADirectoryError: | ||
| print(f"ls: cannot access '{path}': Not a directory") | ||
|
|
||
| def main(): |
There was a problem hiding this comment.
Same question about testing here - If I run ls -a I get different files listed than if I run python3 ls.py -a.
| print(f"ls: cannot access '{path}': No such file or directory") | ||
| except NotADirectoryError: | ||
| print(f"ls: cannot access '{path}': Not a directory") |
There was a problem hiding this comment.
Same question about stdout vs stderr
| print(f"ls: cannot access '{path}': No such file or directory") | ||
| except NotADirectoryError: | ||
| print(f"ls: cannot access '{path}': Not a directory") |
There was a problem hiding this comment.
Stretch goal: What happens if you run ls /some/file for a file not a directory? What does your program do?
| parser.add_argument('paths', nargs='+', help='Files to count') | ||
| args = parser.parse_args() | ||
|
|
||
| total_lines = total_words = total_bytes = 0 |
There was a problem hiding this comment.
It's generally pretty unusual to initialise multiple variables like this in one go, because it makes it harder to skim the code to see where a particular variable was set/initialised than initialising each on its own line.
|
|
||
| if multiple_files: | ||
| parts = [] | ||
| if args.l or not any([args.l, args.w, args.c]): parts.append(str(total_lines)) |
There was a problem hiding this comment.
You're repeating this not any([args.l, args.w, args.c]) three times - can you think of a way to avoid that duplication?
|
|
||
| # Determine what to show | ||
| if not any([count_lines, count_words, count_bytes]): | ||
| count_lines = count_words = count_bytes = True |
There was a problem hiding this comment.
Again, I'd generally avoid putting this all on one line
|
|
||
| print(' '.join(parts), path) | ||
|
|
||
| return (len(lines) if count_lines else 0, |
There was a problem hiding this comment.
It's interesting that you're doing this filtering twice - you're checking whether we should output lines both inside this function and when printing the totals.
Personally, I would probably just always return the values here, and let the caller decide whether to show them.
| if count_lines: parts.append(str(len(lines))) | ||
| if count_words: parts.append(str(len(words))) | ||
| if count_bytes: parts.append(str(bytes_)) |
There was a problem hiding this comment.
It's a little unusual that two of these have len calls and the last doesn't - not a big deal, but a bit oddly inconsistent - I would maybe make these consistent.
Learners, PR Template
Self checklist
Changelist
This PR implements basic versions of the following Unix commands in Python:
These scripts were designed to mimic standard command-line tools and handle typical edge cases like missing files and glob patterns.
Questions